/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Reflection;
using System.Collections;

using Microsoft.Scripting.Generation;
using Microsoft.Scripting.Utils;
using Microsoft.Scripting.Actions;

namespace Microsoft.Scripting.Runtime {
    /// <summary>
    /// Helper methods that calls are generated to from the default DLR binders.
    /// </summary>
    public static class BinderOps {

        #region CreateDelegate support

        /// <summary> Table of dynamicly generated delegates which are shared based upon method signature. </summary>
        private static readonly Publisher<DelegateSignatureInfo, DelegateInfo> _dynamicDelegateCache = new Publisher<DelegateSignatureInfo, DelegateInfo>();

        public static T CreateDelegate<T>(CodeContext context, object callable) {
            return (T)(object)GetDelegate(context, callable, typeof(T));
        }

        /// <summary>
        /// Creates a delegate with a given signature that could be used to invoke this object from non-dynamic code (w/o code context).
        /// A stub is created that makes appropriate conversions/boxing and calls the object.
        /// The stub should be executed within a context of this object's language.
        /// </summary>
        /// <returns>The delegate or a <c>null</c> reference if the object is not callable.</returns>
        public static Delegate GetDelegate(CodeContext context, object callableObject, Type delegateType) {
            ContractUtils.RequiresNotNull(delegateType, "delegateType");

            Delegate result = callableObject as Delegate;
            if (result != null) {
                if (!delegateType.IsAssignableFrom(result.GetType())) {
                    throw RuntimeHelpers.SimpleTypeError(String.Format("Cannot cast {0} to {1}.", result.GetType(), delegateType));
                }

                return result;
            }

            IDynamicObject dynamicObject = callableObject as IDynamicObject;
            if (dynamicObject != null) {

                MethodInfo invoke;

                if (!typeof(Delegate).IsAssignableFrom(delegateType) || (invoke = delegateType.GetMethod("Invoke")) == null) {
                    throw RuntimeHelpers.SimpleTypeError("A specific delegate type is required.");
                }

                ParameterInfo[] parameters = invoke.GetParameters();
                DelegateSignatureInfo signatureInfo = new DelegateSignatureInfo(
                    context,
                    invoke.ReturnType,
                    parameters
                );

                DelegateInfo delegateInfo = _dynamicDelegateCache.GetOrCreateValue(signatureInfo,
                    delegate() {
                        // creation code
                        return signatureInfo.GenerateDelegateStub();
                    });


                result = delegateInfo.CreateDelegate(delegateType, dynamicObject);
                if (result != null) {
                    return result;
                }
            }

            throw RuntimeHelpers.SimpleTypeError("Object is not callable.");
        }

        #endregion

        /// <summary>
        /// Helper function to combine an object array with a sequence of additional parameters that has been splatted for a function call.
        /// </summary>
        public static object[] GetCombinedParameters(object[] initialArgs, object additionalArgs) {
            IList listArgs = additionalArgs as IList;
            if (listArgs == null) {
                IEnumerable ie = additionalArgs as IEnumerable;
                if (ie == null) {
                    throw new InvalidOperationException("args must be iterable");
                }
                listArgs = new List<object>();
                foreach (object o in ie) {
                    listArgs.Add(o);
                }
            }

            object[] res = new object[initialArgs.Length + listArgs.Count];
            Array.Copy(initialArgs, res, initialArgs.Length);
            listArgs.CopyTo(res, initialArgs.Length);
            return res;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "2#")] // TODO: fix
        public static object[] GetCombinedKeywordParameters(object[] initialArgs, IAttributesCollection additionalArgs, ref string[] extraNames) {
            List<object> args = new List<object>(initialArgs);
            List<string> newNames = extraNames == null ? new List<string>(additionalArgs.Count) : new List<string>(extraNames);
            foreach(KeyValuePair<object, object> kvp in additionalArgs) {
                if (kvp.Key is string) {
                    newNames.Add((string)kvp.Key);
                    args.Add(kvp.Value);
                }
            }
            extraNames = newNames.ToArray();
            return args.ToArray();
        }

        public static SymbolDictionary MakeSymbolDictionary(SymbolId[] names, object[] values) {
            SymbolDictionary res = new SymbolDictionary();
            for (int i = 0; i < names.Length; i++) {
                ((IAttributesCollection)res)[names[i]] = values[i];
            }
            return res;
        }

        #region Event support

        public static EventTracker EventTrackerInPlaceAdd<T>(EventTracker self, T target) {
            MethodInfo add = self.Event.GetAddMethod(ScriptDomainManager.Options.PrivateBinding);
            add.Invoke(null, new object[] { target });
            return self;
        }

        public static EventTracker EventTrackerInPlaceRemove<T>(EventTracker self, T target) {
            MethodInfo remove = self.Event.GetRemoveMethod(ScriptDomainManager.Options.PrivateBinding);
            remove.Invoke(null, new object[] { target });
            return self;
        }

        public static BoundMemberTracker BoundEventTrackerInPlaceAdd<T>(BoundMemberTracker self, T target) {
            if (self.BoundTo.MemberType == TrackerTypes.Event) {
                EventTracker et = (EventTracker)self.BoundTo;

                MethodInfo add = et.Event.GetAddMethod(ScriptDomainManager.Options.PrivateBinding);
                add.Invoke(self.ObjectInstance, new object[] { target });
                return self;
            }
            throw new InvalidOperationException();
        }

        public static BoundMemberTracker BoundEventTrackerInPlaceRemove<T>(BoundMemberTracker self, T target) {
            if (self.BoundTo.MemberType == TrackerTypes.Event) {
                EventTracker et = (EventTracker)self.BoundTo;

                MethodInfo remove = et.Event.GetRemoveMethod(ScriptDomainManager.Options.PrivateBinding);
                remove.Invoke(self.ObjectInstance, new object[] { target });
                return self;
            }
            throw new InvalidOperationException();
        }
        
        #endregion

        public static ArgumentTypeException BadArgumentsForOperation(Operators op, params object[] args) {
            StringBuilder message = new StringBuilder("unsupported operand type(s) for operation ");
            message.Append(op.ToString());
            message.Append(": ");
            string comma = "";

            foreach (object o in args) {
                message.Append(comma);
                message.Append(CompilerHelpers.GetType(o));
                comma = ", ";
            }

            throw new ArgumentTypeException(message.ToString());
        }


        // formalNormalArgumentCount - does not include FuncDefFlags.ArgList and FuncDefFlags.KwDict
        // defaultArgumentCount - How many arguments in the method declaration have a default value?
        // providedArgumentCount - How many arguments are passed in at the call site?
        // hasArgList - Is the method declaration of the form "foo(*argList)"?
        // keywordArgumentsProvided - Does the call site specify keyword arguments?
        public static ArgumentTypeException TypeErrorForIncorrectArgumentCount(
            string methodName,
            int formalNormalArgumentCount,
            int defaultArgumentCount,
            int providedArgumentCount,
            bool hasArgList,
            bool keywordArgumentsProvided) {
            return TypeErrorForIncorrectArgumentCount(methodName, formalNormalArgumentCount, formalNormalArgumentCount, defaultArgumentCount, providedArgumentCount, hasArgList, keywordArgumentsProvided);
        }

        public static ArgumentTypeException TypeErrorForIncorrectArgumentCount(
            string methodName,
            int minFormalNormalArgumentCount,
            int maxFormalNormalArgumentCount,
            int defaultArgumentCount,
            int providedArgumentCount,
            bool hasArgList,
            bool keywordArgumentsProvided) {

            int formalCount;
            string formalCountQualifier;
            string nonKeyword = keywordArgumentsProvided ? "non-keyword " : "";

            if (defaultArgumentCount > 0 || hasArgList || minFormalNormalArgumentCount != maxFormalNormalArgumentCount) {
                if (providedArgumentCount < minFormalNormalArgumentCount || maxFormalNormalArgumentCount == Int32.MaxValue) {
                    formalCountQualifier = "at least";
                    formalCount = minFormalNormalArgumentCount - defaultArgumentCount;
                } else {
                    formalCountQualifier = "at most";
                    formalCount = maxFormalNormalArgumentCount;
                }
            } else if (minFormalNormalArgumentCount == 0) {
                return RuntimeHelpers.SimpleTypeError(string.Format("{0}() takes no arguments ({1} given)", methodName, providedArgumentCount));
            } else {
                formalCountQualifier = "exactly";
                formalCount = minFormalNormalArgumentCount;
            }

            return new ArgumentTypeException(string.Format(
                "{0}() takes {1} {2} {3}argument{4} ({5} given)",
                                methodName, // 0
                                formalCountQualifier, // 1
                                formalCount, // 2
                                nonKeyword, // 3
                                formalCount == 1 ? "" : "s", // 4
                                providedArgumentCount)); // 5
        }

        public static ArgumentTypeException TypeErrorForIncorrectArgumentCount(string name, int formalNormalArgumentCount, int defaultArgumentCount, int providedArgumentCount) {
            return TypeErrorForIncorrectArgumentCount(name, formalNormalArgumentCount, defaultArgumentCount, providedArgumentCount, false, false);
        }

        public static ArgumentTypeException TypeErrorForIncorrectArgumentCount(string name, int expected, int received) {
            return TypeErrorForIncorrectArgumentCount(name, expected, 0, received);
        }

        public static ArgumentTypeException TypeErrorForExtraKeywordArgument(string name, string argumentName) {
            return new ArgumentTypeException(String.Format("{0}() got an unexpected keyword argument '{1}'", name, argumentName));
        }

        public static ArgumentTypeException TypeErrorForDuplicateKeywordArgument(string name, string argumentName) {
            return new ArgumentTypeException(String.Format("{0}() got multiple values for keyword argument '{1}'", name, argumentName));
        }

        public static ArgumentTypeException SimpleTypeError(string message) {
            return new ArgumentTypeException(message);
        }

        public static object InvokeMethod(MethodBase mb, object obj, object[] args) {
            try {
                return mb.Invoke(obj, args);
            } catch (TargetInvocationException tie) {
                throw tie.InnerException;
            }
        }

        public static object InvokeConstructor(ConstructorInfo ci, object[] args) {
            try {
                return ci.Invoke(args);
            } catch (TargetInvocationException tie) {
                throw tie.InnerException;
            }
        }
    }
}
